/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package ciphertextattack;

/**
 *
 * @author radik
 */
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.util.*;
import java.nio.file.Paths;
import java.nio.charset.Charset;
import java.io.IOException;
import java.io.FileOutputStream;
import static java.lang.System.*;

public class CipherTextAttack {
    /**
     * @param args the command line arguments
     */
    private static int NUM_OF_BLOCKS_LIMIT = 500;
    private static int MAX_WORD_LENGTH = 120;
    private static int NUM_OF_CANDIDATES =5;
    public static int m_block_size = 10;
    public static int m_key_size = 8;
    private static Map<Byte, Byte> m_key_dic = new HashMap<Byte, Byte>();
    public static Byte[] m_IV;
    public static byte[] m_key_guess = {'a','b','c','d','e','f','g','h'};
    public static byte[] m_key_best = {'a','b','c','d','e','f','g','h'};
    public static String IV_path = "";
    public static String plain_text_path = "";
    public static String key_path = "";
    public static String encrypted_path = "plainText_encrypted.txt";
    public static String final_key_path = "cipherText_key.txt";
    
    private static Map<String, Boolean> m_most_common = new HashMap<String, Boolean>();
    
    public static void main(String[] args) {
        try {

            
            m_most_common = utilities.CreateDictionary();

            switch(args[0]){
                case "Encryption":
                    if (args.length<4) throw new Exception("Not enough arguments given. please try again..");
                    
                    key_path = args[2];
                    m_key_dic = utilities.createKeyDic(key_path);
                    
                    IV_path = args[3];
                    m_IV = utilities.createIV(IV_path);
                    
                    plain_text_path = args[1];
                    byte[] b_plain_text = utilities.readFile(plain_text_path);
                    
                    byte[] cyphered_text = CBC(b_plain_text);
                    utilities.writeToFile(cyphered_text, encrypted_path);
                    break;

                case "Decryption":
                    if (args.length<3) throw new Exception("Not enough arguments given. please try again..");
                    
                    encrypted_path = args[1];
                    IV_path = args[2];
                    
                    long startTime = System.nanoTime();
                    Map<Character, Character> ans_key = AttackKey(encrypted_path);
                    long endTime = System.nanoTime();
                    long duration = endTime - startTime;
                    double seconds = (double)duration / 1000000000.0;
                    utilities.writeToFile(ans_key, final_key_path);
                    break;

                default: out.println("Wrong arguments given. please try again..");
            }
        }
       catch(Exception e) {
           out.println(e.getMessage());
       }
       finally {
            out.println("Done!");
        }
    }
    
    public static Map<Character, Character> AttackKey(String encrypted_file) throws Exception
    {
        m_IV =  utilities.createIV(IV_path);
        byte[] encrypted_data = utilities.readFile(encrypted_file);
        boolean is_not_valid = true;
        LinkedList<Map.Entry<byte[], Integer>> key_score = new LinkedList<Map.Entry<byte[], Integer>>(); 
        while(is_not_valid)
        {
            int curr_score = CheckKeys(encrypted_data);
            //option 3 save best NUM_OF_CANDIDATES using linked-list
            if (key_score.size() <NUM_OF_CANDIDATES  || curr_score > key_score.getLast().getValue() )
            {
                byte[] current_key = Arrays.copyOf(m_key_guess, m_key_guess.length);
                AddKeyToList(key_score, current_key, curr_score);
            }
            is_not_valid = getNext();
            
        }
        m_key_best = key_score.getFirst().getKey();
        return ReverseKey();
    }
    
    private static Map<Character, Character> ReverseKey()
    {
        byte[] letters = {'a','b','c','d','e','f','g','h'};
        Map<Character, Character> key_dic = new HashMap<Character, Character>();
        //byte[] keys = Arrays.copyOf(m_key_best, m_key_size);
        for (int i = 0; i < m_key_best.length; i++) {
            key_dic.put((char)m_key_best[i], (char)letters[i]);
        }
        return key_dic;
    }
    
    private static int CheckKeys(byte[] encrypted_data)
    {
        Stemmer stemmer = new Stemmer();
        Map<Byte,Byte> dic = SetKeyDicionary();
        Byte[] arr_IV = Arrays.copyOf(m_IV, m_block_size);
        char[] decrypted_data =new char[MAX_WORD_LENGTH];
        int num_blocks = encrypted_data.length/m_block_size;        
        Byte value = 0;
        int lengI = Math.min(num_blocks-1, NUM_OF_BLOCKS_LIMIT);     
        int k = 0,common_counter =0;
        for(int i=0; i<lengI; i++)
        {
            for (int j=0;j<m_block_size;j++) 
            {
                int index = (i*m_block_size)+j;
                byte encrypted_value = encrypted_data[index];
                value = dic.get(encrypted_value);
                if (value== null) {
                    value = encrypted_value;
                }
                byte xor_value = (byte)(value^arr_IV[j]);
                decrypted_data[k] = (char)xor_value;
                k++;
                arr_IV[j] = encrypted_data[index];
                if (((char)xor_value == ' ' || (char)xor_value == '\n')) {
                    String word = new String(decrypted_data, 0 , k-1).toLowerCase();
                    if (m_most_common.containsKey(stemmer.StemWord(word)))  common_counter++;
                    decrypted_data = new char[MAX_WORD_LENGTH];
                    k=0;
                }
            }
        }

        return common_counter;
    }
    
    private static Map<Byte,Byte> SetKeyDicionary()
    {
        byte[] letters = {'a','b','c','d','e','f','g','h'};
        //byte[] correct_key = {'c','g','e','a','h','f','d','b'};
        Map<Byte,Byte> dic = new HashMap<Byte, Byte>();
        for (int i = 0; i < letters.length; i++) {
            dic.put(letters[i], m_key_guess[i]);
        }
        return dic;
    }
    
    public static boolean getNext()
    {
        int i = m_key_size - 1;
        while (m_key_guess[i-1] >= m_key_guess[i]) {
            i = i-1;
            if (i == 0) return false;
        }

        int j = m_key_size;
        while (m_key_guess[j-1] <= m_key_guess[i-1]) {
            j = j-1;
            if (j == 0) return false;
  
        }
        
        //swap(Value, i-1, j-1);    // swap values at positions (i-1) and (j-1)
        byte temp = m_key_guess[i-1];
        m_key_guess[i-1] = m_key_guess[j-1];
        m_key_guess[j-1] = temp;

        i++; j = m_key_size;
        while (i < j)
        {
            //swap(Value, i-1, j-1);
            temp = m_key_guess[i-1];
            m_key_guess[i-1] = m_key_guess[j-1];
            m_key_guess[j-1] = temp;
            i++;
            j--;
        }
        return true;
    }
    
    public static byte[] CBC(byte[] plain_text) {   
        double num_blocks = (double)plain_text.length/(double)m_block_size;      
        int cypher_text_size = (int) Math.ceil(num_blocks)*m_block_size;
        byte[] cypher_text = new byte[cypher_text_size];
        Byte value = 0;
        int i;
        
        for (i=0;i<(int)num_blocks;i++) {
            for (int j=0;j<m_block_size;j++) {
                int index = (i*m_block_size)+j;
                Byte xor_value = (byte)(plain_text[index]^m_IV[j]);
                
                value = m_key_dic.get(xor_value);
                if (value!= null) {
                    cypher_text[index] = value;
                }
                else cypher_text[index] = xor_value;
                m_IV[j] = cypher_text[index];
            }
        }
        
        for (int k=0;k<m_block_size && i*m_block_size < plain_text.length; k++) {
            int index = (i*m_block_size)+k;
            Byte xor_value =0;
            if (index<plain_text.length) {
                xor_value = (byte)(plain_text[index]^m_IV[k]);
            }
            else {
                xor_value = (byte)(0^m_IV[k]);
            }
            value = m_key_dic.get(xor_value);
            if (value!= null) {
                cypher_text[index] = value;
            }
            else cypher_text[index] = xor_value;
        }
        return cypher_text;
    }

    private static void AddKeyToList(LinkedList<Map.Entry<byte[], Integer>> key_score, byte[] current_key, int curr_score) {
        boolean wasAdded = false;
        int size = key_score.size();
        Map.Entry<byte[],Integer> entry = new AbstractMap.SimpleEntry<byte[], Integer>(current_key, curr_score);
        for (int i = 0; i < size; i++) {
            if (curr_score > key_score.get(i).getValue())
            {
                key_score.add(i, entry);
                wasAdded = true;
                break;
            }
        }
        if (!wasAdded) key_score.addLast(entry);
        if(key_score.size()> NUM_OF_CANDIDATES) key_score.removeLast();
    }

    
}
